<?php
/**
 * TCP通讯封装
 */
declare(strict_types=1);

namespace Libraries\Connections\Drivers;

use Libraries\Helper;
use ErrorException;

class TcpSocket
{
    private $objSocket = null;
    private $strHost;
    private $intPort;
    
    /**
     * 创建Socket
     * @param string $strHost
     * @param int $intPort
     * @throws ErrorException
     */
    public function __construct(string $strHost = '127.0.0.1', int $intPort = 8081)
    {
        $this->strHost = $strHost;
        $this->intPort = $intPort;
        $objSocket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
        if ($objSocket === false) {
            throw new ErrorException(socket_strerror(socket_last_error()));
        }
        if (socket_connect($objSocket, $strHost, $intPort) === false) {
            throw new ErrorException(socket_strerror(socket_last_error()));
        }
        if (socket_set_nonblock($objSocket)) { // 设置为非阻塞的流模式
            $this->objSocket = $objSocket;
        } else {
            throw new ErrorException('fail to set nonblock');
        }
    }
    
    /**
     * 取连接的主机和端口
     * @return array
     */
    public function getHostAndPort(): array
    {
        return [
            'host' => $this->strHost,
            'port' => $this->intPort,
        ];
    }
    
    /**
     * 析构函数，消损连接
     */
    public function __destruct()
    {
        $this->close();
    }

    /**
     * 关闭连接
     * @return void
     */
    public function close(): void
    {
        if ($this->objSocket) {
            socket_close($this->objSocket);
            $this->objSocket = null;
        }
    }
    
    /**
     * 向Socket发出数据，请得到响应内容
     * @param array $arrData
     * @param int $intTimeout
     * @return array|null
     * @throws ErrorException
     */
    public function request(array $arrData, int $intTimeout = 5): ?array
    {
        if (!$this->objSocket) {
            return null;
        }
        $strData = Helper::packTcpMsg($arrData);
        if ($strData && socket_write($this->objSocket, $strData, strlen($strData)) !== false) {
            return $this->read($intTimeout);
        }
        throw new ErrorException('Socket send fail.');
    }

    /**
     * 从Socket读取数据
     * @param int $intTimeout
     * @return array
     * @throws ErrorException
     */
    private function read(int $intTimeout = 5): array
    {
        $strRe = '';
        $intLen = null;
        $intGotTotal = 0;
        $blnEnd = false;
        $floStartTime = microtime(true);
        while (!$blnEnd) {
            if (round((microtime(true) - $floStartTime), 2) > ($intTimeout * 0.9)) { // 超时判断
                throw new ErrorException('Socket read time out, more than ' . $intTimeout . ' (s)');
            }
            usleep(10); // 暂停十万分之一秒
            $strTemp = socket_read($this->objSocket, $intLen ? 4096 : 2, PHP_BINARY_READ);
            if ($strTemp !== false) {
                $intLenTemp = strlen($strTemp);
                if (!$intLenTemp) {
                    continue;
                }
                if ($intLen) {
                    $strRe .= $strTemp;
                    $intGotTotal += $intLenTemp;
                    $blnEnd = ($intGotTotal >= $intLen);
                } else {
                    $arrTemp = unpack('n_len', $strTemp);
                    if ($arrTemp && isset($arrTemp['_len']) && is_int($arrTemp['_len'])) {
                        $intLen = $arrTemp['_len'];
                    }
                }
                unset($strTemp);
            } elseif (socket_last_error() == 11) { // EAGAIN，当前无数据
                socket_clear_error($this->objSocket); // 清除错误
            } else { // 读取异常
                throw new ErrorException('Socket read get un-know error.');
            }
        }
        $arrRe = Helper::jsonDecode(trim($strRe), true);
        is_array($arrRe) || $arrRe = [];
        return $arrRe;
    }
}
